Passed
Branch v8.x (792d5d)
by Rafael S.
02:23
created

index.js ➔ toDataURI   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
/*
2
 * Copyright (c) 2017-2018 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import bitDepthLib from './vendor/bitdepth.js';
33
import * as imaadpcm from './vendor/imaadpcm.js';
34
import * as alawmulaw from './vendor/alawmulaw.js';
35
import {encode, decode} from './vendor/base64-arraybuffer-es6.js';
36
import {unpackArray, packArrayTo, unpackArrayTo,
37
pack, packStringTo, packTo, packString} from './vendor/byte-data.js';
38
import {wavHeader, AUDIO_FORMATS} from './lib/wavheader.js';
39
import riffChunks from './vendor/riff-chunks.js';
40
import BufferIO from './lib/bufferio.js';
41
42
/**
43
 * Class representing a wav file.
44
 * @ignore
45
 */
46
export default class WaveFile {
47
48
  /**
49
   * @param {?Uint8Array} bytes A wave file buffer.
50
   * @throws {Error} If no 'RIFF' chunk is found.
51
   * @throws {Error} If no 'fmt ' chunk is found.
52
   * @throws {Error} If no 'data' chunk is found.
53
   */
54
  constructor(bytes=null) {
55
    /**
56
     * The container identifier.
57
     * 'RIFF', 'RIFX' and 'RF64' are supported.
58
     * @type {string}
59
     */
60
    this.container = '';
61
    /**
62
     * @type {number}
63
     */
64
    this.chunkSize = 0;
65
    /**
66
     * The format.
67
     * Always 'WAVE'.
68
     * @type {string}
69
     */
70
    this.format = '';
71
    /**
72
     * The data of the 'fmt' chunk.
73
     * @type {!Object<string, *>}
74
     */
75
    this.fmt = {
76
      /** @type {string} */
77
      chunkId: '',
78
      /** @type {number} */
79
      chunkSize: 0,
80
      /** @type {number} */
81
      audioFormat: 0,
82
      /** @type {number} */
83
      numChannels: 0,
84
      /** @type {number} */
85
      sampleRate: 0,
86
      /** @type {number} */
87
      byteRate: 0,
88
      /** @type {number} */
89
      blockAlign: 0,
90
      /** @type {number} */
91
      bitsPerSample: 0,
92
      /** @type {number} */
93
      cbSize: 0,
94
      /** @type {number} */
95
      validBitsPerSample: 0,
96
      /** @type {number} */
97
      dwChannelMask: 0,
98
      /**
99
       * 4 32-bit values representing a 128-bit ID
100
       * @type {!Array<number>}
101
       */
102
      subformat: []
103
    };
104
    /**
105
     * The data of the 'fact' chunk.
106
     * @type {!Object<string, *>}
107
     */
108
    this.fact = {
109
      /** @type {string} */
110
      chunkId: '',
111
      /** @type {number} */
112
      chunkSize: 0,
113
      /** @type {number} */
114
      dwSampleLength: 0
115
    };
116
    /**
117
     * The data of the 'cue ' chunk.
118
     * @type {!Object<string, *>}
119
     */
120
    this.cue = {
121
      /** @type {string} */
122
      chunkId: '',
123
      /** @type {number} */
124
      chunkSize: 0,
125
      /** @type {number} */
126
      dwCuePoints: 0,
127
      /** @type {!Array<!Object>} */
128
      points: [],
129
    };
130
    /**
131
     * The data of the 'smpl' chunk.
132
     * @type {!Object<string, *>}
133
     */
134
    this.smpl = {
135
      /** @type {string} */
136
      chunkId: '',
137
      /** @type {number} */
138
      chunkSize: 0,
139
      /** @type {number} */
140
      dwManufacturer: 0,
141
      /** @type {number} */
142
      dwProduct: 0,
143
      /** @type {number} */
144
      dwSamplePeriod: 0,
145
      /** @type {number} */
146
      dwMIDIUnityNote: 0,
147
      /** @type {number} */
148
      dwMIDIPitchFraction: 0,
149
      /** @type {number} */
150
      dwSMPTEFormat: 0,
151
      /** @type {number} */
152
      dwSMPTEOffset: 0,
153
      /** @type {number} */
154
      dwNumSampleLoops: 0,
155
      /** @type {number} */
156
      dwSamplerData: 0,
157
      /** @type {!Array<!Object>} */
158
      loops: []
159
    };
160
    /**
161
     * The data of the 'bext' chunk.
162
     * @type {!Object<string, *>}
163
     */
164
    this.bext = {
165
      /** @type {string} */
166
      chunkId: '',
167
      /** @type {number} */
168
      chunkSize: 0,
169
      /** @type {string} */
170
      description: '', //256
171
      /** @type {string} */
172
      originator: '', //32
173
      /** @type {string} */
174
      originatorReference: '', //32
175
      /** @type {string} */
176
      originationDate: '', //10
177
      /** @type {string} */
178
      originationTime: '', //8
179
      /**
180
       * 2 32-bit values, timeReference high and low
181
       * @type {!Array<number>}
182
       */
183
      timeReference: [0, 0],
184
      /** @type {number} */
185
      version: 0, //WORD
186
      /** @type {string} */
187
      UMID: '', // 64 chars
188
      /** @type {number} */
189
      loudnessValue: 0, //WORD
190
      /** @type {number} */
191
      loudnessRange: 0, //WORD
192
      /** @type {number} */
193
      maxTruePeakLevel: 0, //WORD
194
      /** @type {number} */
195
      maxMomentaryLoudness: 0, //WORD
196
      /** @type {number} */
197
      maxShortTermLoudness: 0, //WORD
198
      /** @type {string} */
199
      reserved: '', //180
200
      /** @type {string} */
201
      codingHistory: '' // string, unlimited
202
    };
203
    /**
204
     * The data of the 'ds64' chunk.
205
     * Used only with RF64 files.
206
     * @type {!Object<string, *>}
207
     */
208
    this.ds64 = {
209
      /** @type {string} */
210
      chunkId: '',
211
      /** @type {number} */
212
      chunkSize: 0,
213
      /** @type {number} */
214
      riffSizeHigh: 0, // DWORD
215
      /** @type {number} */
216
      riffSizeLow: 0, // DWORD
217
      /** @type {number} */
218
      dataSizeHigh: 0, // DWORD
219
      /** @type {number} */
220
      dataSizeLow: 0, // DWORD
221
      /** @type {number} */
222
      originationTime: 0, // DWORD
223
      /** @type {number} */
224
      sampleCountHigh: 0, // DWORD
225
      /** @type {number} */
226
      sampleCountLow: 0 // DWORD
227
      /** @type {number} */
228
      //'tableLength': 0, // DWORD
229
      /** @type {!Array<number>} */
230
      //'table': []
231
    };
232
    /**
233
     * The data of the 'data' chunk.
234
     * @type {!Object<string, *>}
235
     */
236
    this.data = {
237
      /** @type {string} */
238
      chunkId: '',
239
      /** @type {number} */
240
      chunkSize: 0,
241
      /** @type {!Uint8Array} */
242
      samples: new Uint8Array(0)
243
    };
244
    /**
245
     * The data of the 'LIST' chunks.
246
     * Each item in this list look like this:
247
     *  {
248
     *      chunkId: '',
249
     *      chunkSize: 0,
250
     *      format: '',
251
     *      subChunks: []
252
     *   }
253
     * @type {!Array<!Object>}
254
     */
255
    this.LIST = [];
256
    /**
257
     * The data of the 'junk' chunk.
258
     * @type {!Object<string, *>}
259
     */
260
    this.junk = {
261
      /** @type {string} */
262
      chunkId: '',
263
      /** @type {number} */
264
      chunkSize: 0,
265
      /** @type {!Array<number>} */
266
      chunkData: []
267
    };
268
    /**
269
     * @type {!Object}
270
     * @private
271
     */
272
    this.uInt16_ = {bits: 16, be: false};
273
    /**
274
     * @type {!Object}
275
     * @private
276
     */
277
    this.uInt32_ = {bits: 32, be: false};
278
    /**
279
     * The bit depth code according to the samples.
280
     * @type {string}
281
     */
282
    this.bitDepth = '0';
283
    /**
284
     * @type {!Object}
285
     * @private
286
     */
287
    this.dataType = {};
288
    this.io = new BufferIO();
289
    // Load a file from the buffer if one was passed
290
    // when creating the object
291
    if(bytes) {
292
      this.fromBuffer(bytes);
293
    }
294
  }
295
296
  /**
297
   * Set up the WaveFile object based on the arguments passed.
298
   * @param {number} numChannels The number of channels
299
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
300
   * @param {number} sampleRate The sample rate.
301
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
302
   * @param {string} bitDepthCode The audio bit depth code.
303
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
304
   *    or any value between '8' and '32' (like '12').
305
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples
306
   *    The samples. Must be in the correct range according to the bit depth.
307
   * @param {?Object} options Optional. Used to force the container
308
   *    as RIFX with {'container': 'RIFX'}
309
   * @throws {Error} If any argument does not meet the criteria.
310
   */
311
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
312
    if (!options.container) {
313
      options.container = 'RIFF';
314
    }
315
    this.container = options.container;
316
    this.bitDepth = bitDepthCode;
317
    samples = this.interleave_(samples);
318
    /** @type {number} */
319
    let numBytes = (((parseInt(bitDepthCode, 10) - 1) | 7) + 1) / 8;
320
    this.updateDataType_();
321
    this.data.samples = new Uint8Array(samples.length * numBytes);
322
    packArrayTo(samples, this.dataType, this.data.samples);
323
    /** @type {!Object} */
324
    let header = wavHeader(
325
      bitDepthCode, numChannels, sampleRate,
326
      numBytes, this.data.samples.length, options);
327
    this.clearHeader_();
328
    this.chunkSize = header.chunkSize;
329
    this.format = header.format;
330
    this.fmt = header.fmt;
331
    if (header.fact) {
332
      this.fact = header.fact;
333
    }
334
    this.data.chunkId = 'data';
335
    this.data.chunkSize = this.data.samples.length;
336
    this.validateHeader_();
337
    this.LEorBE_();
338
  }
339
340
  /**
341
   * Set up the WaveFile object from a byte buffer.
342
   * @param {!Uint8Array} bytes The buffer.
343
   * @param {boolean=} samples True if the samples should be loaded.
344
   * @throws {Error} If container is not RIFF, RIFX or RF64.
345
   * @throws {Error} If no 'fmt ' chunk is found.
346
   * @throws {Error} If no 'data' chunk is found.
347
   */
348
  fromBuffer(bytes, samples=true) {
349
    this.readWavBuffer(bytes, samples);
350
  }
351
352
  /**
353
   * Return a byte buffer representig the WaveFile object as a .wav file.
354
   * The return value of this method can be written straight to disk.
355
   * @return {!Uint8Array} A .wav file.
356
   * @throws {Error} If any property of the object appears invalid.
357
   */
358
  toBuffer() {
359
    this.validateHeader_();
360
    return this.createWaveFile_();
361
  }
362
363
  /**
364
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
365
   * @param {string} base64String A .wav file as a base64 string.
366
   * @throws {Error} If any property of the object appears invalid.
367
   */
368
  fromBase64(base64String) {
369
    this.fromBuffer(new Uint8Array(decode(base64String)));
370
  }
371
372
  /**
373
   * Return a base64 string representig the WaveFile object as a .wav file.
374
   * @return {string} A .wav file as a base64 string.
375
   * @throws {Error} If any property of the object appears invalid.
376
   */
377
  toBase64() {
378
    /** @type {!Uint8Array} */
379
    let buffer = this.toBuffer();
380
    return encode(buffer, 0, buffer.length);
381
  }
382
383
  /**
384
   * Return a DataURI string representig the WaveFile object as a .wav file.
385
   * The return of this method can be used to load the audio in browsers.
386
   * @return {string} A .wav file as a DataURI.
387
   * @throws {Error} If any property of the object appears invalid.
388
   */
389
  toDataURI() {
390
    return 'data:audio/wav;base64,' + this.toBase64();
391
  }
392
393
  /**
394
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
395
   * @param {string} dataURI A .wav file as DataURI.
396
   * @throws {Error} If any property of the object appears invalid.
397
   */
398
  fromDataURI(dataURI) {
399
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
400
  }
401
402
  /**
403
   * Force a file as RIFF.
404
   */
405
  toRIFF() {
406
    if (this.container == 'RF64') {
407
      this.fromScratch(
408
        this.fmt.numChannels,
409
        this.fmt.sampleRate,
410
        this.bitDepth,
411
        unpackArray(this.data.samples, this.dataType));
412
    } else {
413
      this.dataType.be = true;
414
      this.fromScratch(
415
        this.fmt.numChannels,
416
        this.fmt.sampleRate,
417
        this.bitDepth,
418
        unpackArray(this.data.samples, this.dataType));
419
    }
420
  }
421
422
  /**
423
   * Force a file as RIFX.
424
   */
425
  toRIFX() {
426
    if (this.container == 'RF64') {
427
      this.fromScratch(
428
        this.fmt.numChannels,
429
        this.fmt.sampleRate,
430
        this.bitDepth,
431
        unpackArray(this.data.samples, this.dataType),
432
        {container: 'RIFX'});
433
    } else {
434
      this.fromScratch(
435
        this.fmt.numChannels,
436
        this.fmt.sampleRate,
437
        this.bitDepth,
438
        unpackArray(this.data.samples, this.dataType),
439
        {container: 'RIFX'});
440
    }
441
  }
442
443
  /**
444
   * Change the bit depth of the samples.
445
   * @param {string} newBitDepth The new bit depth of the samples.
446
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
447
   * @param {boolean} changeResolution A boolean indicating if the
448
   *    resolution of samples should be actually changed or not.
449
   * @throws {Error} If the bit depth is not valid.
450
   */
451
  toBitDepth(newBitDepth, changeResolution=true) {
452
    // @type {string}
453
    let toBitDepth = newBitDepth;
454
    // @type {string}
455
    let thisBitDepth = this.bitDepth;
456
    if (!changeResolution) {
457
      toBitDepth = this.realBitDepth_(newBitDepth);
458
      thisBitDepth = this.realBitDepth_(this.bitDepth);
459
    }
460
    this.assureUncompressed_();
461
    // @type {number}
462
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
463
    // @type {!Float64Array}
464
    let typedSamplesInput = new Float64Array(sampleCount + 1);
465
    // @type {!Float64Array}
466
    let typedSamplesOutput = new Float64Array(sampleCount + 1);
467
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
468
    bitDepthLib(
469
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
470
    this.fromScratch(
471
      this.fmt.numChannels,
472
      this.fmt.sampleRate,
473
      newBitDepth,
474
      typedSamplesOutput,
475
      {container: this.correctContainer_()});
476
  }
477
478
  /**
479
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
480
   * @throws {Error} If sample rate is not 8000.
481
   * @throws {Error} If number of channels is not 1.
482
   */
483
  toIMAADPCM() {
484
    if (this.fmt.sampleRate !== 8000) {
485
      throw new Error(
486
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
487
    } else if(this.fmt.numChannels !== 1) {
488
      throw new Error(
489
        'Only mono files can be compressed as IMA-ADPCM.');
490
    } else {
491
      this.assure16Bit_();
492
      let output = new Int16Array(this.data.samples.length / 2);
493
      unpackArrayTo(this.data.samples, this.dataType, output);
494
      this.fromScratch(
495
        this.fmt.numChannels,
496
        this.fmt.sampleRate,
497
        '4',
498
        imaadpcm.encode(output),
499
        {container: this.correctContainer_()});
500
    }
501
  }
502
503
  /**
504
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
505
   * @param {string} bitDepthCode The new bit depth of the samples.
506
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
507
   *    Optional. Default is 16.
508
   */
509
  fromIMAADPCM(bitDepthCode='16') {
510
    this.fromScratch(
511
      this.fmt.numChannels,
512
      this.fmt.sampleRate,
513
      '16',
514
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
515
      {container: this.correctContainer_()});
516
    if (bitDepthCode != '16') {
517
      this.toBitDepth(bitDepthCode);
518
    }
519
  }
520
521
  /**
522
   * Encode a 16-bit wave file as 8-bit A-Law.
523
   */
524
  toALaw() {
525
    this.assure16Bit_();
526
    let output = new Int16Array(this.data.samples.length / 2);
527
    unpackArrayTo(this.data.samples, this.dataType, output);
528
    this.fromScratch(
529
      this.fmt.numChannels,
530
      this.fmt.sampleRate,
531
      '8a',
532
      alawmulaw.alaw.encode(output),
533
      {container: this.correctContainer_()});
534
  }
535
536
  /**
537
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
538
   * @param {string} bitDepthCode The new bit depth of the samples.
539
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
540
   *    Optional. Default is 16.
541
   */
542
  fromALaw(bitDepthCode='16') {
543
    this.fromScratch(
544
      this.fmt.numChannels,
545
      this.fmt.sampleRate,
546
      '16',
547
      alawmulaw.alaw.decode(this.data.samples),
548
      {container: this.correctContainer_()});
549
    if (bitDepthCode != '16') {
550
      this.toBitDepth(bitDepthCode);
551
    }
552
  }
553
554
  /**
555
   * Encode 16-bit wave file as 8-bit mu-Law.
556
   */
557
  toMuLaw() {
558
    this.assure16Bit_();
559
    let output = new Int16Array(this.data.samples.length / 2);
560
    unpackArrayTo(this.data.samples, this.dataType, output);
561
    this.fromScratch(
562
      this.fmt.numChannels,
563
      this.fmt.sampleRate,
564
      '8m',
565
      alawmulaw.mulaw.encode(output),
566
      {container: this.correctContainer_()});
567
  }
568
569
  /**
570
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
571
   * @param {string} bitDepthCode The new bit depth of the samples.
572
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
573
   *    Optional. Default is 16.
574
   */
575
  fromMuLaw(bitDepthCode='16') {
576
    this.fromScratch(
577
      this.fmt.numChannels,
578
      this.fmt.sampleRate,
579
      '16',
580
      alawmulaw.mulaw.decode(this.data.samples),
581
      {container: this.correctContainer_()});
582
    if (bitDepthCode != '16') {
583
      this.toBitDepth(bitDepthCode);
584
    }
585
  }
586
587
  /**
588
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
589
   * then it is created. It if exists, it is overwritten.
590
   * @param {string} tag The tag name.
591
   * @param {string} value The tag value.
592
   * @throws {Error} If the tag name is not valid.
593
   */
594
  setTag(tag, value) {
595
    tag = this.fixTagName_(tag);
596
    /** @type {!Object} */
597
    let index = this.getTagIndex_(tag);
598
    if (index.TAG !== null) {
599
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
600
        value.length + 1;
601
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
602
    } else if (index.LIST !== null) {
603
      this.LIST[index.LIST].subChunks.push({
604
        chunkId: tag,
605
        chunkSize: value.length + 1,
606
        value: value});
607
    } else {
608
      this.LIST.push({
609
        chunkId: 'LIST',
610
        chunkSize: 8 + value.length + 1,
611
        format: 'INFO',
612
        subChunks: []});
613
      this.LIST[this.LIST.length - 1].subChunks.push({
614
        chunkId: tag,
615
        chunkSize: value.length + 1,
616
        value: value});
617
    }
618
  }
619
620
  /**
621
   * Return the value of a RIFF tag in the INFO chunk.
622
   * @param {string} tag The tag name.
623
   * @return {?string} The value if the tag is found, null otherwise.
624
   */
625
  getTag(tag) {
626
    /** @type {!Object} */
627
    let index = this.getTagIndex_(tag);
628
    if (index.TAG !== null) {
629
      return this.LIST[index.LIST].subChunks[index.TAG].value;
630
    }
631
    return null;
632
  }
633
634
  /**
635
   * Remove a RIFF tag in the INFO chunk.
636
   * @param {string} tag The tag name.
637
   * @return {boolean} True if a tag was deleted.
638
   */
639
  deleteTag(tag) {
640
    /** @type {!Object} */
641
    let index = this.getTagIndex_(tag);
642
    if (index.TAG !== null) {
643
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
644
      return true;
645
    }
646
    return false;
647
  }
648
649
  /**
650
   * Create a cue point in the wave file.
651
   * @param {number} position The cue point position in milliseconds.
652
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
653
   */
654
  setCuePoint(position, labl='') {
655
    this.cue.chunkId = 'cue ';
656
    position = (position * this.fmt.sampleRate) / 1000;
657
    /** @type {!Array<!Object>} */
658
    let existingPoints = this.getCuePoints_();
659
    this.clearLISTadtl_();
660
    /** @type {number} */
661
    let len = this.cue.points.length;
662
    this.cue.points = [];
663
    /** @type {boolean} */
664
    let hasSet = false;
665
    if (len === 0) {
666
      this.setCuePoint_(position, 1, labl);
667
    } else {
668
      for (let i=0; i<len; i++) {
669
        if (existingPoints[i].dwPosition > position && !hasSet) {
670
          this.setCuePoint_(position, i + 1, labl);
671
          this.setCuePoint_(
672
            existingPoints[i].dwPosition,
673
            i + 2,
674
            existingPoints[i].label);
675
          hasSet = true;
676
        } else {
677
          this.setCuePoint_(
678
            existingPoints[i].dwPosition,
679
            i + 1,
680
            existingPoints[i].label);
681
        }
682
      }
683
      if (!hasSet) {
684
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
685
      }
686
    }
687
    this.cue.dwCuePoints = this.cue.points.length;
688
  }
689
690
  /**
691
   * Remove a cue point from a wave file.
692
   * @param {number} index the index of the point. First is 1,
693
   *    second is 2, and so on.
694
   */
695
  deleteCuePoint(index) {
696
    this.cue.chunkId = 'cue ';
697
    /** @type {!Array<!Object>} */
698
    let existingPoints = this.getCuePoints_();
699
    this.clearLISTadtl_();
700
    /** @type {number} */
701
    let len = this.cue.points.length;
702
    this.cue.points = [];
703
    for (let i=0; i<len; i++) {
704
      if (i + 1 !== index) {
705
        this.setCuePoint_(
706
          existingPoints[i].dwPosition,
707
          i + 1,
708
          existingPoints[i].label);
709
      }
710
    }
711
    this.cue.dwCuePoints = this.cue.points.length;
712
    if (this.cue.dwCuePoints) {
713
      this.cue.chunkId = 'cue ';
714
    } else {
715
      this.cue.chunkId = '';
716
      this.clearLISTadtl_();
717
    }
718
  }
719
720
  /**
721
   * Update the label of a cue point.
722
   * @param {number} pointIndex The ID of the cue point.
723
   * @param {string} label The new text for the label.
724
   */
725
  updateLabel(pointIndex, label) {
726
    /** @type {?number} */
727
    let adtlIndex = this.getAdtlChunk_();
728
    if (adtlIndex !== null) {
729
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
730
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
731
            pointIndex) {
732
          this.LIST[adtlIndex].subChunks[i].value = label;
733
        }
734
      }
735
    }
736
  }
737
738
  /**
739
   * Make the file 16-bit if it is not.
740
   * @private
741
   */
742
  assure16Bit_() {
743
    this.assureUncompressed_();
744
    if (this.bitDepth != '16') {
745
      this.toBitDepth('16');
746
    }
747
  }
748
749
  /**
750
   * Uncompress the samples in case of a compressed file.
751
   * @private
752
   */
753
  assureUncompressed_() {
754
    if (this.bitDepth == '8a') {
755
      this.fromALaw();
756
    } else if(this.bitDepth == '8m') {
757
      this.fromMuLaw();
758
    } else if (this.bitDepth == '4') {
759
      this.fromIMAADPCM();
760
    }
761
  }
762
763
  /**
764
   * Set up the WaveFile object from a byte buffer.
765
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples The samples.
766
   * @private
767
   */
768
  interleave_(samples) {
769
    if (samples.length > 0) {
770
      if (samples[0].constructor === Array) {
771
        /** @type {!Array<number>} */
772
        let finalSamples = [];
773
        for (let i=0; i < samples[0].length; i++) {
774
          for (let j=0; j < samples.length; j++) {
775
            finalSamples.push(samples[j][i]);
776
          }
777
        }
778
        samples = finalSamples;
779
      }
780
    }
781
    return samples;
782
  }
783
784
  /**
785
   * Push a new cue point in this.cue.points.
786
   * @param {number} position The position in milliseconds.
787
   * @param {number} dwName the dwName of the cue point
788
   * @private
789
   */
790
  setCuePoint_(position, dwName, label) {
791
    this.cue.points.push({
792
      dwName: dwName,
793
      dwPosition: position,
794
      fccChunk: 'data',
795
      dwChunkStart: 0,
796
      dwBlockStart: 0,
797
      dwSampleOffset: position,
798
    });
799
    this.setLabl_(dwName, label);
800
  }
801
802
  /**
803
   * Return an array with the position of all cue points in the file.
804
   * @return {!Array<!Object>}
805
   * @private
806
   */
807
  getCuePoints_() {
808
    /** @type {!Array<!Object>} */
809
    let points = [];
810
    for (let i=0; i<this.cue.points.length; i++) {
811
      points.push({
812
        dwPosition: this.cue.points[i].dwPosition,
813
        label: this.getLabelForCuePoint_(
814
          this.cue.points[i].dwName)});
815
    }
816
    return points;
817
  }
818
819
  /**
820
   * Return the label of a cue point.
821
   * @param {number} pointDwName The ID of the cue point.
822
   * @return {string}
823
   * @private
824
   */
825
  getLabelForCuePoint_(pointDwName) {
826
    /** @type {?number} */
827
    let adtlIndex = this.getAdtlChunk_();
828
    if (adtlIndex !== null) {
829
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
830
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
831
            pointDwName) {
832
          return this.LIST[adtlIndex].subChunks[i].value;
833
        }
834
      }
835
    }
836
    return '';
837
  }
838
839
  /**
840
   * Clear any LIST chunk labeled as 'adtl'.
841
   * @private
842
   */
843
  clearLISTadtl_() {
844
    for (let i=0; i<this.LIST.length; i++) {
845
      if (this.LIST[i].format == 'adtl') {
846
        this.LIST.splice(i);
847
      }
848
    }
849
  }
850
851
  /**
852
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
853
   * @param {number} dwName The ID of the cue point.
854
   * @param {string} label The label for the cue point.
855
   * @private
856
   */
857
  setLabl_(dwName, label) {
858
    /** @type {?number} */
859
    let adtlIndex = this.getAdtlChunk_();
860
    if (adtlIndex === null) {
861
      this.LIST.push({
862
        chunkId: 'LIST',
863
        chunkSize: 4,
864
        format: 'adtl',
865
        subChunks: []});
866
      adtlIndex = this.LIST.length - 1;
867
    }
868
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
869
  }
870
871
  /**
872
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
873
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
874
   * @param {number} dwName The ID of the cue point.
875
   * @param {string} label The label for the cue point.
876
   * @private
877
   */
878
  setLabelText_(adtlIndex, dwName, label) {
879
    this.LIST[adtlIndex].subChunks.push({
880
      chunkId: 'labl',
881
      chunkSize: label.length,
882
      dwName: dwName,
883
      value: label
884
    });
885
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
886
  }
887
888
  /**
889
   * Return the index of the 'adtl' LIST in this.LIST.
890
   * @return {?number}
891
   * @private
892
   */
893
  getAdtlChunk_() {
894
    for (let i=0; i<this.LIST.length; i++) {
895
      if(this.LIST[i].format == 'adtl') {
896
        return i;
897
      }
898
    }
899
    return null;
900
  }
901
902
  /**
903
   * Return the index of a tag in a FILE chunk.
904
   * @param {string} tag The tag name.
905
   * @return {!Object<string, ?number>}
906
   *    Object.LIST is the INFO index in LIST
907
   *    Object.TAG is the tag index in the INFO
908
   * @private
909
   */
910
  getTagIndex_(tag) {
911
    /** @type {!Object<string, ?number>} */
912
    let index = {LIST: null, TAG: null};
913
    for (let i=0; i<this.LIST.length; i++) {
914
      if (this.LIST[i].format == 'INFO') {
915
        index.LIST = i;
916
        for (let j=0; j<this.LIST[i].subChunks.length; j++) {
917
          if (this.LIST[i].subChunks[j].chunkId == tag) {
918
            index.TAG = j;
919
            break;
920
          }
921
        }
922
        break;
923
      }
924
    }
925
    return index;
926
  }
927
928
  /**
929
   * Fix a RIFF tag format if possible, throw an error otherwise.
930
   * @param {string} tag The tag name.
931
   * @return {string} The tag name in proper fourCC format.
932
   * @private
933
   */
934
  fixTagName_(tag) {
935
    if (tag.constructor !== String) {
936
      throw new Error('Invalid tag name.');
937
    } else if(tag.length < 4) {
938
      for (let i=0; i<4-tag.length; i++) {
939
        tag += ' ';
940
      }
941
    }
942
    return tag;
943
  }
944
945
  /**
946
   * Set up the WaveFile object from a byte buffer.
947
   * @param {!Uint8Array} buffer The buffer.
948
   * @param {boolean=} samples True if the samples should be loaded.
949
   * @throws {Error} If container is not RIFF, RIFX or RF64.
950
   * @throws {Error} If no 'fmt ' chunk is found.
951
   * @throws {Error} If no 'data' chunk is found.
952
   */
953
  readWavBuffer(buffer, samples=true) {
954
    this.io.head_ = 0;
955
    this.clearHeader_();
956
    this.readRIFFChunk_(buffer);
957
    /** @type {!Object} */
958
    let chunk = riffChunks(buffer);
959
    this.readDs64Chunk_(buffer, chunk.subChunks);
960
    this.readFmtChunk_(buffer, chunk.subChunks);
961
    this.readFactChunk_(buffer, chunk.subChunks);
962
    this.readBextChunk_(buffer, chunk.subChunks);
963
    this.readCueChunk_(buffer, chunk.subChunks);
964
    this.readSmplChunk_(buffer, chunk.subChunks);
965
    this.readDataChunk_(buffer, chunk.subChunks, samples);
966
    this.readJunkChunk_(buffer, chunk.subChunks);
967
    this.readLISTChunk_(buffer, chunk.subChunks);
968
    this.bitDepthFromFmt_();
969
    this.updateDataType_();
970
  }
971
972
  /**
973
   * Read the RIFF chunk a wave file.
974
   * @param {!Uint8Array} bytes A wav buffer.
975
   * @throws {Error} If no 'RIFF' chunk is found.
976
   * @private
977
   */
978
  readRIFFChunk_(bytes) {
979
    this.io.head_ = 0;
980
    this.container = this.io.readString_(bytes, 4);
981
    if (['RIFF', 'RIFX', 'RF64'].indexOf(this.container) === -1) {
982
      throw Error('Not a supported format.');
983
    }
984
    this.LEorBE_();
985
    this.chunkSize = this.io.read_(bytes, this.uInt32_);
986
    this.format = this.io.readString_(bytes, 4);
987
    if (this.format != 'WAVE') {
988
      throw Error('Could not find the "WAVE" format identifier');
989
    }
990
  }
991
992
  /**
993
   * Read the 'fmt ' chunk of a wave file.
994
   * @param {!Uint8Array} buffer The wav file buffer.
995
   * @param {!Object} signature The file signature.
996
   * @throws {Error} If no 'fmt ' chunk is found.
997
   * @private
998
   */
999
  readFmtChunk_(buffer, signature) {
1000
    /** @type {?Object} */
1001
    let chunk = this.findChunk_(signature, 'fmt ');
1002
    if (chunk) {
1003
      this.io.head_ = chunk.chunkData.start;
1004
      this.fmt.chunkId = chunk.chunkId;
1005
      this.fmt.chunkSize = chunk.chunkSize;
1006
      this.fmt.audioFormat = this.io.read_(buffer, this.uInt16_);
1007
      this.fmt.numChannels = this.io.read_(buffer, this.uInt16_);
1008
      this.fmt.sampleRate = this.io.read_(buffer, this.uInt32_);
1009
      this.fmt.byteRate = this.io.read_(buffer, this.uInt32_);
1010
      this.fmt.blockAlign = this.io.read_(buffer, this.uInt16_);
1011
      this.fmt.bitsPerSample = this.io.read_(buffer, this.uInt16_);
1012
      this.readFmtExtension_(buffer);
1013
    } else {
1014
      throw Error('Could not find the "fmt " chunk');
1015
    }
1016
  }
1017
1018
  /**
1019
   * Read the 'fmt ' chunk extension.
1020
   * @param {!Uint8Array} buffer The wav file buffer.
1021
   * @private
1022
   */
1023
  readFmtExtension_(buffer) {
1024
    if (this.fmt.chunkSize > 16) {
1025
      this.fmt.cbSize = this.io.read_(buffer, this.uInt16_);
1026
      if (this.fmt.chunkSize > 18) {
1027
        this.fmt.validBitsPerSample = this.io.read_(buffer, this.uInt16_);
1028
        if (this.fmt.chunkSize > 20) {
1029
          this.fmt.dwChannelMask = this.io.read_(buffer, this.uInt32_);
1030
          this.fmt.subformat = [
1031
            this.io.read_(buffer, this.uInt32_),
1032
            this.io.read_(buffer, this.uInt32_),
1033
            this.io.read_(buffer, this.uInt32_),
1034
            this.io.read_(buffer, this.uInt32_)];
1035
        }
1036
      }
1037
    }
1038
  }
1039
1040
  /**
1041
   * Read the 'fact' chunk of a wav file.
1042
   * @param {!Uint8Array} buffer The wav file buffer.
1043
   * @param {!Object} signature The file signature.
1044
   * @private
1045
   */
1046
  readFactChunk_(buffer, signature) {
1047
    /** @type {?Object} */
1048
    let chunk = this.findChunk_(signature, 'fact');
1049
    if (chunk) {
1050
      this.io.head_ = chunk.chunkData.start;
1051
      this.fact.chunkId = chunk.chunkId;
1052
      this.fact.chunkSize = chunk.chunkSize;
1053
      this.fact.dwSampleLength = this.io.read_(buffer, this.uInt32_);
1054
    }
1055
  }
1056
1057
  /**
1058
   * Read the 'cue ' chunk of a wave file.
1059
   * @param {!Uint8Array} buffer The wav file buffer.
1060
   * @param {!Object} signature The file signature.
1061
   * @private
1062
   */
1063
  readCueChunk_(buffer, signature) {
1064
    /** @type {?Object} */
1065
    let chunk = this.findChunk_(signature, 'cue ');
1066
    if (chunk) {
1067
      this.io.head_ = chunk.chunkData.start;
1068
      this.cue.chunkId = chunk.chunkId;
1069
      this.cue.chunkSize = chunk.chunkSize;
1070
      this.cue.dwCuePoints = this.io.read_(buffer, this.uInt32_);
1071
      for (let i=0; i<this.cue.dwCuePoints; i++) {
1072
        this.cue.points.push({
1073
          dwName: this.io.read_(buffer, this.uInt32_),
1074
          dwPosition: this.io.read_(buffer, this.uInt32_),
1075
          fccChunk: this.io.readString_(buffer, 4),
1076
          dwChunkStart: this.io.read_(buffer, this.uInt32_),
1077
          dwBlockStart: this.io.read_(buffer, this.uInt32_),
1078
          dwSampleOffset: this.io.read_(buffer, this.uInt32_),
1079
        });
1080
      }
1081
    }
1082
  }
1083
1084
  /**
1085
   * Read the 'smpl' chunk of a wave file.
1086
   * @param {!Uint8Array} buffer The wav file buffer.
1087
   * @param {!Object} signature The file signature.
1088
   * @private
1089
   */
1090
  readSmplChunk_(buffer, signature) {
1091
    /** @type {?Object} */
1092
    let chunk = this.findChunk_(signature, 'smpl');
1093
    if (chunk) {
1094
      this.io.head_ = chunk.chunkData.start;
1095
      this.smpl.chunkId = chunk.chunkId;
1096
      this.smpl.chunkSize = chunk.chunkSize;
1097
      this.smpl.dwManufacturer = this.io.read_(buffer, this.uInt32_);
1098
      this.smpl.dwProduct = this.io.read_(buffer, this.uInt32_);
1099
      this.smpl.dwSamplePeriod = this.io.read_(buffer, this.uInt32_);
1100
      this.smpl.dwMIDIUnityNote = this.io.read_(buffer, this.uInt32_);
1101
      this.smpl.dwMIDIPitchFraction = this.io.read_(buffer, this.uInt32_);
1102
      this.smpl.dwSMPTEFormat = this.io.read_(buffer, this.uInt32_);
1103
      this.smpl.dwSMPTEOffset = this.io.read_(buffer, this.uInt32_);
1104
      this.smpl.dwNumSampleLoops = this.io.read_(buffer, this.uInt32_);
1105
      this.smpl.dwSamplerData = this.io.read_(buffer, this.uInt32_);
1106
      for (let i=0; i<this.smpl.dwNumSampleLoops; i++) {
1107
        this.smpl.loops.push({
1108
          dwName: this.io.read_(buffer, this.uInt32_),
1109
          dwType: this.io.read_(buffer, this.uInt32_),
1110
          dwStart: this.io.read_(buffer, this.uInt32_),
1111
          dwEnd: this.io.read_(buffer, this.uInt32_),
1112
          dwFraction: this.io.read_(buffer, this.uInt32_),
1113
          dwPlayCount: this.io.read_(buffer, this.uInt32_),
1114
        });
1115
      }
1116
    }
1117
  }
1118
1119
  /**
1120
   * Read the 'data' chunk of a wave file.
1121
   * @param {!Uint8Array} buffer The wav file buffer.
1122
   * @param {!Object} signature The file signature.
1123
   * @param {boolean} samples True if the samples should be loaded.
1124
   * @throws {Error} If no 'data' chunk is found.
1125
   * @private
1126
   */
1127
  readDataChunk_(buffer, signature, samples) {
1128
    /** @type {?Object} */
1129
    let chunk = this.findChunk_(signature, 'data');
1130
    if (chunk) {
1131
      this.data.chunkId = 'data';
1132
      this.data.chunkSize = chunk.chunkSize;
1133
      if (samples) {
1134
        this.data.samples = buffer.slice(
1135
          chunk.chunkData.start,
1136
          chunk.chunkData.end);
1137
      }
1138
    } else {
1139
      throw Error('Could not find the "data" chunk');
1140
    }
1141
  }
1142
1143
  /**
1144
   * Read the 'bext' chunk of a wav file.
1145
   * @param {!Uint8Array} buffer The wav file buffer.
1146
   * @param {!Object} signature The file signature.
1147
   * @private
1148
   */
1149
  readBextChunk_(buffer, signature) {
1150
    /** @type {?Object} */
1151
    let chunk = this.findChunk_(signature, 'bext');
1152
    if (chunk) {
1153
      this.io.head_ = chunk.chunkData.start;
1154
      this.bext.chunkId = chunk.chunkId;
1155
      this.bext.chunkSize = chunk.chunkSize;
1156
      this.bext.description = this.io.readString_(buffer, 256);
1157
      this.bext.originator = this.io.readString_(buffer, 32);
1158
      this.bext.originatorReference = this.io.readString_(buffer, 32);
1159
      this.bext.originationDate = this.io.readString_(buffer, 10);
1160
      this.bext.originationTime = this.io.readString_(buffer, 8);
1161
      this.bext.timeReference = [
1162
        this.io.read_(buffer, this.uInt32_),
1163
        this.io.read_(buffer, this.uInt32_)];
1164
      this.bext.version = this.io.read_(buffer, this.uInt16_);
1165
      this.bext.UMID = this.io.readString_(buffer, 64);
1166
      this.bext.loudnessValue = this.io.read_(buffer, this.uInt16_);
1167
      this.bext.loudnessRange = this.io.read_(buffer, this.uInt16_);
1168
      this.bext.maxTruePeakLevel = this.io.read_(buffer, this.uInt16_);
1169
      this.bext.maxMomentaryLoudness = this.io.read_(buffer, this.uInt16_);
1170
      this.bext.maxShortTermLoudness = this.io.read_(buffer, this.uInt16_);
1171
      this.bext.reserved = this.io.readString_(buffer, 180);
1172
      this.bext.codingHistory = this.io.readString_(
1173
        buffer, this.bext.chunkSize - 602);
1174
    }
1175
  }
1176
1177
  /**
1178
   * Read the 'ds64' chunk of a wave file.
1179
   * @param {!Uint8Array} buffer The wav file buffer.
1180
   * @param {!Object} signature The file signature.
1181
   * @throws {Error} If no 'ds64' chunk is found and the file is RF64.
1182
   * @private
1183
   */
1184
  readDs64Chunk_(buffer, signature) {
1185
    /** @type {?Object} */
1186
    let chunk = this.findChunk_(signature, 'ds64');
1187
    if (chunk) {
1188
      this.io.head_ = chunk.chunkData.start;
1189
      this.ds64.chunkId = chunk.chunkId;
1190
      this.ds64.chunkSize = chunk.chunkSize;
1191
      this.ds64.riffSizeHigh = this.io.read_(buffer, this.uInt32_);
1192
      this.ds64.riffSizeLow = this.io.read_(buffer, this.uInt32_);
1193
      this.ds64.dataSizeHigh = this.io.read_(buffer, this.uInt32_);
1194
      this.ds64.dataSizeLow = this.io.read_(buffer, this.uInt32_);
1195
      this.ds64.originationTime = this.io.read_(buffer, this.uInt32_);
1196
      this.ds64.sampleCountHigh = this.io.read_(buffer, this.uInt32_);
1197
      this.ds64.sampleCountLow = this.io.read_(buffer, this.uInt32_);
1198
      //if (this.ds64.chunkSize > 28) {
1199
      //  this.ds64.tableLength = unpack(
1200
      //    chunkData.slice(28, 32), this.uInt32_);
1201
      //  this.ds64.table = chunkData.slice(
1202
      //     32, 32 + this.ds64.tableLength);
1203
      //}
1204
    } else {
1205
      if (this.container == 'RF64') {
1206
        throw Error('Could not find the "ds64" chunk');
1207
      }
1208
    }
1209
  }
1210
1211
  /**
1212
   * Read the 'LIST' chunks of a wave file.
1213
   * @param {!Uint8Array} buffer The wav file buffer.
1214
   * @param {!Object} signature The file signature.
1215
   * @private
1216
   */
1217
  readLISTChunk_(buffer, signature) {
1218
    /** @type {?Object} */
1219
    let listChunks = this.findChunk_(signature, 'LIST', true);
1220
    if (listChunks === null) {
1221
      return;
1222
    }
1223
    for (let j=0; j < listChunks.length; j++) {
1224
      /** @type {!Object} */
1225
      let subChunk = listChunks[j];
1226
      this.LIST.push({
1227
        chunkId: subChunk.chunkId,
1228
        chunkSize: subChunk.chunkSize,
1229
        format: subChunk.format,
1230
        subChunks: []});
1231
      for (let x=0; x<subChunk.subChunks.length; x++) {
1232
        this.readLISTSubChunks_(subChunk.subChunks[x],
1233
          subChunk.format, buffer);
1234
      }
1235
    }
1236
  }
1237
1238
  /**
1239
   * Read the sub chunks of a 'LIST' chunk.
1240
   * @param {!Object} subChunk The 'LIST' subchunks.
1241
   * @param {string} format The 'LIST' format, 'adtl' or 'INFO'.
1242
   * @param {!Uint8Array} buffer The wav file buffer.
1243
   * @private
1244
   */
1245
  readLISTSubChunks_(subChunk, format, buffer) {
1246
    if (format == 'adtl') {
1247
      if (['labl', 'note','ltxt'].indexOf(subChunk.chunkId) > -1) {
1248
        this.io.head_ = subChunk.chunkData.start;
1249
        /** @type {!Object<string, string|number>} */
1250
        let item = {
1251
          chunkId: subChunk.chunkId,
1252
          chunkSize: subChunk.chunkSize,
1253
          dwName: this.io.read_(buffer, this.uInt32_)
1254
        };
1255
        if (subChunk.chunkId == 'ltxt') {
1256
          item.dwSampleLength = this.io.read_(buffer, this.uInt32_);
1257
          item.dwPurposeID = this.io.read_(buffer, this.uInt32_);
1258
          item.dwCountry = this.io.read_(buffer, this.uInt16_);
1259
          item.dwLanguage = this.io.read_(buffer, this.uInt16_);
1260
          item.dwDialect = this.io.read_(buffer, this.uInt16_);
1261
          item.dwCodePage = this.io.read_(buffer, this.uInt16_);
1262
        }
1263
        item.value = this.io.readZSTR_(buffer, this.io.head_);
1264
        this.LIST[this.LIST.length - 1].subChunks.push(item);
1265
      }
1266
    // RIFF INFO tags like ICRD, ISFT, ICMT
1267
    } else if(format == 'INFO') {
1268
      this.io.head_ = subChunk.chunkData.start;
1269
      this.LIST[this.LIST.length - 1].subChunks.push({
1270
        chunkId: subChunk.chunkId,
1271
        chunkSize: subChunk.chunkSize,
1272
        value: this.io.readZSTR_(buffer, this.io.head_)
1273
      });
1274
    }
1275
  }
1276
1277
  /**
1278
   * Read the 'junk' chunk of a wave file.
1279
   * @param {!Uint8Array} buffer The wav file buffer.
1280
   * @param {!Object} signature The file signature.
1281
   * @private
1282
   */
1283
  readJunkChunk_(buffer, signature) {
1284
    /** @type {?Object} */
1285
    let chunk = this.findChunk_(signature, 'junk');
1286
    if (chunk) {
1287
      this.junk = {
1288
        chunkId: chunk.chunkId,
1289
        chunkSize: chunk.chunkSize,
1290
        chunkData: [].slice.call(buffer.slice(
1291
          chunk.chunkData.start,
1292
          chunk.chunkData.end))
1293
      };
1294
    }
1295
  }
1296
1297
  /**
1298
   * Return the bytes of the 'bext' chunk.
1299
   * @return {!Array<number>} The 'bext' chunk bytes.
1300
   * @private
1301
   */
1302
  getBextBytes_() {
1303
    /** @type {!Array<number>} */
1304
    let bytes = [];
1305
    this.enforceBext_();
1306
    if (this.bext.chunkId) {
1307
      this.bext.chunkSize = 602 + this.bext.codingHistory.length;
1308
      bytes = bytes.concat(
1309
        packString(this.bext.chunkId),
1310
        pack(602 + this.bext.codingHistory.length, this.uInt32_),
1311
        this.io.writeString_(this.bext.description, 256),
1312
        this.io.writeString_(this.bext.originator, 32),
1313
        this.io.writeString_(this.bext.originatorReference, 32),
1314
        this.io.writeString_(this.bext.originationDate, 10),
1315
        this.io.writeString_(this.bext.originationTime, 8),
1316
        pack(this.bext.timeReference[0], this.uInt32_),
1317
        pack(this.bext.timeReference[1], this.uInt32_),
1318
        pack(this.bext.version, this.uInt16_),
1319
        this.io.writeString_(this.bext.UMID, 64),
1320
        pack(this.bext.loudnessValue, this.uInt16_),
1321
        pack(this.bext.loudnessRange, this.uInt16_),
1322
        pack(this.bext.maxTruePeakLevel, this.uInt16_),
1323
        pack(this.bext.maxMomentaryLoudness, this.uInt16_),
1324
        pack(this.bext.maxShortTermLoudness, this.uInt16_),
1325
        this.io.writeString_(this.bext.reserved, 180),
1326
        this.io.writeString_(
1327
          this.bext.codingHistory, this.bext.codingHistory.length));
1328
    }
1329
    return bytes;
1330
  }
1331
1332
  /**
1333
   * Make sure a 'bext' chunk is created if BWF data was created in a file.
1334
   * @private
1335
   */
1336
  enforceBext_() {
1337
    for (var prop in this.bext) {
1338
      if (this.bext.hasOwnProperty(prop)) {
1339
        if (this.bext[prop] && prop != 'timeReference') {
1340
          this.bext.chunkId = 'bext';
1341
          break;
1342
        }
1343
      }
1344
    }
1345
    if (this.bext.timeReference[0] || this.bext.timeReference[1]) {
1346
      this.bext.chunkId = 'bext';
1347
    }
1348
  }
1349
1350
  /**
1351
   * Return the bytes of the 'ds64' chunk.
1352
   * @return {!Array<number>} The 'ds64' chunk bytes.
1353
   * @private
1354
   */
1355
  getDs64Bytes_() {
1356
    /** @type {!Array<number>} */
1357
    let bytes = [];
1358
    if (this.ds64.chunkId) {
1359
      bytes = bytes.concat(
1360
        packString(this.ds64.chunkId),
1361
        pack(this.ds64.chunkSize, this.uInt32_),
1362
        pack(this.ds64.riffSizeHigh, this.uInt32_),
1363
        pack(this.ds64.riffSizeLow, this.uInt32_),
1364
        pack(this.ds64.dataSizeHigh, this.uInt32_),
1365
        pack(this.ds64.dataSizeLow, this.uInt32_),
1366
        pack(this.ds64.originationTime, this.uInt32_),
1367
        pack(this.ds64.sampleCountHigh, this.uInt32_),
1368
        pack(this.ds64.sampleCountLow, this.uInt32_));
1369
    }
1370
    //if (this.ds64.tableLength) {
1371
    //  ds64Bytes = ds64Bytes.concat(
1372
    //    pack(this.ds64.tableLength, this.uInt32_),
1373
    //    this.ds64.table);
1374
    //}
1375
    return bytes;
1376
  }
1377
1378
  /**
1379
   * Return the bytes of the 'cue ' chunk.
1380
   * @return {!Array<number>} The 'cue ' chunk bytes.
1381
   * @private
1382
   */
1383
  getCueBytes_() {
1384
    /** @type {!Array<number>} */
1385
    let bytes = [];
1386
    if (this.cue.chunkId) {
1387
      /** @type {!Array<number>} */
1388
      let cuePointsBytes = this.getCuePointsBytes_();
1389
      bytes = bytes.concat(
1390
        packString(this.cue.chunkId),
1391
        pack(cuePointsBytes.length + 4, this.uInt32_),
1392
        pack(this.cue.dwCuePoints, this.uInt32_),
1393
        cuePointsBytes);
1394
    }
1395
    return bytes;
1396
  }
1397
1398
  /**
1399
   * Return the bytes of the 'cue ' points.
1400
   * @return {!Array<number>} The 'cue ' points as an array of bytes.
1401
   * @private
1402
   */
1403
  getCuePointsBytes_() {
1404
    /** @type {!Array<number>} */
1405
    let points = [];
1406
    for (let i=0; i<this.cue.dwCuePoints; i++) {
1407
      points = points.concat(
1408
        pack(this.cue.points[i].dwName, this.uInt32_),
1409
        pack(this.cue.points[i].dwPosition, this.uInt32_),
1410
        packString(this.cue.points[i].fccChunk),
1411
        pack(this.cue.points[i].dwChunkStart, this.uInt32_),
1412
        pack(this.cue.points[i].dwBlockStart, this.uInt32_),
1413
        pack(this.cue.points[i].dwSampleOffset, this.uInt32_));
1414
    }
1415
    return points;
1416
  }
1417
1418
  /**
1419
   * Return the bytes of the 'smpl' chunk.
1420
   * @return {!Array<number>} The 'smpl' chunk bytes.
1421
   * @private
1422
   */
1423
  getSmplBytes_() {
1424
    /** @type {!Array<number>} */
1425
    let bytes = [];
1426
    if (this.smpl.chunkId) {
1427
      /** @type {!Array<number>} */
1428
      let smplLoopsBytes = this.getSmplLoopsBytes_();
1429
      bytes = bytes.concat(
1430
        packString(this.smpl.chunkId),
1431
        pack(smplLoopsBytes.length + 36, this.uInt32_),
1432
        pack(this.smpl.dwManufacturer, this.uInt32_),
1433
        pack(this.smpl.dwProduct, this.uInt32_),
1434
        pack(this.smpl.dwSamplePeriod, this.uInt32_),
1435
        pack(this.smpl.dwMIDIUnityNote, this.uInt32_),
1436
        pack(this.smpl.dwMIDIPitchFraction, this.uInt32_),
1437
        pack(this.smpl.dwSMPTEFormat, this.uInt32_),
1438
        pack(this.smpl.dwSMPTEOffset, this.uInt32_),
1439
        pack(this.smpl.dwNumSampleLoops, this.uInt32_),
1440
        pack(this.smpl.dwSamplerData, this.uInt32_),
1441
        smplLoopsBytes);
1442
    }
1443
    return bytes;
1444
  }
1445
1446
  /**
1447
   * Return the bytes of the 'smpl' loops.
1448
   * @return {!Array<number>} The 'smpl' loops as an array of bytes.
1449
   * @private
1450
   */
1451
  getSmplLoopsBytes_() {
1452
    /** @type {!Array<number>} */
1453
    let loops = [];
1454
    for (let i=0; i<this.smpl.dwNumSampleLoops; i++) {
1455
      loops = loops.concat(
1456
        pack(this.smpl.loops[i].dwName, this.uInt32_),
1457
        pack(this.smpl.loops[i].dwType, this.uInt32_),
1458
        pack(this.smpl.loops[i].dwStart, this.uInt32_),
1459
        pack(this.smpl.loops[i].dwEnd, this.uInt32_),
1460
        pack(this.smpl.loops[i].dwFraction, this.uInt32_),
1461
        pack(this.smpl.loops[i].dwPlayCount, this.uInt32_));
1462
    }
1463
    return loops;
1464
  }
1465
1466
  /**
1467
   * Return the bytes of the 'fact' chunk.
1468
   * @return {!Array<number>} The 'fact' chunk bytes.
1469
   * @private
1470
   */
1471
  getFactBytes_() {
1472
    /** @type {!Array<number>} */
1473
    let bytes = [];
1474
    if (this.fact.chunkId) {
1475
      bytes = bytes.concat(
1476
        packString(this.fact.chunkId),
1477
        pack(this.fact.chunkSize, this.uInt32_),
1478
        pack(this.fact.dwSampleLength, this.uInt32_));
1479
    }
1480
    return bytes;
1481
  }
1482
1483
  /**
1484
   * Return the bytes of the 'fmt ' chunk.
1485
   * @return {!Array<number>} The 'fmt' chunk bytes.
1486
   * @throws {Error} if no 'fmt ' chunk is present.
1487
   * @private
1488
   */
1489
  getFmtBytes_() {
1490
    /** @type {!Array<number>} */
1491
    let fmtBytes = [];
1492
    if (this.fmt.chunkId) {
1493
      return fmtBytes.concat(
1494
        packString(this.fmt.chunkId),
1495
        pack(this.fmt.chunkSize, this.uInt32_),
1496
        pack(this.fmt.audioFormat, this.uInt16_),
1497
        pack(this.fmt.numChannels, this.uInt16_),
1498
        pack(this.fmt.sampleRate, this.uInt32_),
1499
        pack(this.fmt.byteRate, this.uInt32_),
1500
        pack(this.fmt.blockAlign, this.uInt16_),
1501
        pack(this.fmt.bitsPerSample, this.uInt16_),
1502
        this.getFmtExtensionBytes_());
1503
    }
1504
    throw Error('Could not find the "fmt " chunk');
1505
  }
1506
1507
  /**
1508
   * Return the bytes of the fmt extension fields.
1509
   * @return {!Array<number>} The fmt extension bytes.
1510
   * @private
1511
   */
1512
  getFmtExtensionBytes_() {
1513
    /** @type {!Array<number>} */
1514
    let extension = [];
1515
    if (this.fmt.chunkSize > 16) {
1516
      extension = extension.concat(
1517
        pack(this.fmt.cbSize, this.uInt16_));
1518
    }
1519
    if (this.fmt.chunkSize > 18) {
1520
      extension = extension.concat(
1521
        pack(this.fmt.validBitsPerSample, this.uInt16_));
1522
    }
1523
    if (this.fmt.chunkSize > 20) {
1524
      extension = extension.concat(
1525
        pack(this.fmt.dwChannelMask, this.uInt32_));
1526
    }
1527
    if (this.fmt.chunkSize > 24) {
1528
      extension = extension.concat(
1529
        pack(this.fmt.subformat[0], this.uInt32_),
1530
        pack(this.fmt.subformat[1], this.uInt32_),
1531
        pack(this.fmt.subformat[2], this.uInt32_),
1532
        pack(this.fmt.subformat[3], this.uInt32_));
1533
    }
1534
    return extension;
1535
  }
1536
1537
  /**
1538
   * Return the bytes of the 'LIST' chunk.
1539
   * @return {!Array<number>} The 'LIST' chunk bytes.
1540
   */
1541
  getLISTBytes_() {
1542
    /** @type {!Array<number>} */
1543
    let bytes = [];
1544
    for (let i=0; i<this.LIST.length; i++) {
1545
      /** @type {!Array<number>} */
1546
      let subChunksBytes = this.getLISTSubChunksBytes_(
1547
          this.LIST[i].subChunks, this.LIST[i].format);
1548
      bytes = bytes.concat(
1549
        packString(this.LIST[i].chunkId),
1550
        pack(subChunksBytes.length + 4, this.uInt32_),
1551
        packString(this.LIST[i].format),
1552
        subChunksBytes);
1553
    }
1554
    return bytes;
1555
  }
1556
1557
  /**
1558
   * Return the bytes of the sub chunks of a 'LIST' chunk.
1559
   * @param {!Array<!Object>} subChunks The 'LIST' sub chunks.
1560
   * @param {string} format The format of the 'LIST' chunk.
1561
   *    Currently supported values are 'adtl' or 'INFO'.
1562
   * @return {!Array<number>} The sub chunk bytes.
1563
   * @private
1564
   */
1565
  getLISTSubChunksBytes_(subChunks, format) {
1566
    /** @type {!Array<number>} */
1567
    let bytes = [];
1568
    for (let i=0; i<subChunks.length; i++) {
1569
      if (format == 'INFO') {
1570
        bytes = bytes.concat(
1571
          packString(subChunks[i].chunkId),
1572
          pack(subChunks[i].value.length + 1, this.uInt32_),
1573
          this.io.writeString_(
1574
            subChunks[i].value, subChunks[i].value.length));
1575
        bytes.push(0);
1576
      } else if (format == 'adtl') {
1577
        if (['labl', 'note'].indexOf(subChunks[i].chunkId) > -1) {
1578
          bytes = bytes.concat(
1579
            packString(subChunks[i].chunkId),
1580
            pack(
1581
              subChunks[i].value.length + 4 + 1, this.uInt32_),
1582
            pack(subChunks[i].dwName, this.uInt32_),
1583
            this.io.writeString_(
1584
              subChunks[i].value,
1585
              subChunks[i].value.length));
1586
          bytes.push(0);
1587
        } else if (subChunks[i].chunkId == 'ltxt') {
1588
          bytes = bytes.concat(
1589
            this.getLtxtChunkBytes_(subChunks[i]));
1590
        }
1591
      }
1592
      if (bytes.length % 2) {
1593
        bytes.push(0);
1594
      }
1595
    }
1596
    return bytes;
1597
  }
1598
1599
  /**
1600
   * Return the bytes of a 'ltxt' chunk.
1601
   * @param {!Object} ltxt the 'ltxt' chunk.
1602
   * @return {!Array<number>} The 'ltxt' chunk bytes.
1603
   * @private
1604
   */
1605
  getLtxtChunkBytes_(ltxt) {
1606
    return [].concat(
1607
      packString(ltxt.chunkId),
1608
      pack(ltxt.value.length + 20, this.uInt32_),
1609
      pack(ltxt.dwName, this.uInt32_),
1610
      pack(ltxt.dwSampleLength, this.uInt32_),
1611
      pack(ltxt.dwPurposeID, this.uInt32_),
1612
      pack(ltxt.dwCountry, this.uInt16_),
1613
      pack(ltxt.dwLanguage, this.uInt16_),
1614
      pack(ltxt.dwDialect, this.uInt16_),
1615
      pack(ltxt.dwCodePage, this.uInt16_),
1616
      this.io.writeString_(ltxt.value, ltxt.value.length));
1617
  }
1618
1619
  /**
1620
   * Return the bytes of the 'junk' chunk.
1621
   * @return {!Array<number>} The 'junk' chunk bytes.
1622
   * @private
1623
   */
1624
  getJunkBytes_() {
1625
    /** @type {!Array<number>} */
1626
    let bytes = [];
1627
    if (this.junk.chunkId) {
1628
      return bytes.concat(
1629
        packString(this.junk.chunkId),
1630
        pack(this.junk.chunkData.length, this.uInt32_),
1631
        this.junk.chunkData);
1632
    }
1633
    return bytes;
1634
  }
1635
1636
  /**
1637
   * Return a .wav file byte buffer with the data from the WaveFile object.
1638
   * The return value of this method can be written straight to disk.
1639
   * @return {!Uint8Array} The wav file bytes.
1640
   * @private
1641
   */
1642
  createWaveFile_() {
1643
    /** @type {!Array<!Array<number>>} */
1644
    let fileBody = [
1645
      this.getJunkBytes_(),
1646
      this.getDs64Bytes_(),
1647
      this.getBextBytes_(),
1648
      this.getFmtBytes_(),
1649
      this.getFactBytes_(),
1650
      packString(this.data.chunkId),
1651
      pack(this.data.samples.length, this.uInt32_),
1652
      this.data.samples,
1653
      this.getCueBytes_(),
1654
      this.getSmplBytes_(),
1655
      this.getLISTBytes_()
1656
    ];
1657
    /** @type {number} */
1658
    let fileBodyLength = 0;
1659
    for (let i=0; i<fileBody.length; i++) {
1660
      fileBodyLength += fileBody[i].length;
1661
    }
1662
    /** @type {!Uint8Array} */
1663
    let file = new Uint8Array(fileBodyLength + 12);
1664
    /** @type {number} */
1665
    let index = 0;
1666
    index = packStringTo(this.container, file, index);
1667
    index = packTo(fileBodyLength + 4, this.uInt32_, file, index);
1668
    index = packStringTo(this.format, file, index);
1669
    for (let i=0; i<fileBody.length; i++) {
1670
      file.set(fileBody[i], index);
1671
      index += fileBody[i].length;
1672
    }
1673
    return file;
1674
  }
1675
1676
  /**
1677
   * Reset attributes that should emptied when a file is
1678
   * created with the fromScratch() or fromBuffer() methods.
1679
   * @private
1680
   */
1681
  clearHeader_() {
1682
    this.fmt.cbSize = 0;
1683
    this.fmt.validBitsPerSample = 0;
1684
    this.fact.chunkId = '';
1685
    this.ds64.chunkId = '';
1686
  }
1687
1688
  /**
1689
   * Set up to work wih big-endian or little-endian files.
1690
   * The types used are changed to LE or BE. If the
1691
   * the file is big-endian (RIFX), true is returned.
1692
   * @return {boolean} True if the file is RIFX.
1693
   * @private
1694
   */
1695
  LEorBE_() {
1696
    /** @type {boolean} */
1697
    let bigEndian = this.container === 'RIFX';
1698
    this.uInt16_.be = bigEndian;
1699
    this.uInt32_.be = bigEndian;
1700
    return bigEndian;
1701
  }
1702
1703
  /**
1704
   * Find a chunk by its fourCC_ in a array of RIFF chunks.
1705
   * @param {!Object} chunks The wav file chunks.
1706
   * @param {string} chunkId The chunk fourCC_.
1707
   * @param {boolean} multiple True if there may be multiple chunks
1708
   *    with the same chunkId.
1709
   * @return {?Array<!Object>}
1710
   * @private
1711
   */
1712
  findChunk_(chunks, chunkId, multiple=false) {
1713
    /** @type {!Array<!Object>} */
1714
    let chunk = [];
1715
    for (let i=0; i<chunks.length; i++) {
1716
      if (chunks[i].chunkId == chunkId) {
1717
        if (multiple) {
1718
          chunk.push(chunks[i]);
1719
        } else {
1720
          return chunks[i];
1721
        }
1722
      }
1723
    }
1724
    if (chunkId == 'LIST') {
1725
      return chunk.length ? chunk : null;
1726
    }
1727
    return null;
1728
  }
1729
1730
  /**
1731
   * Update the type definition used to read and write the samples.
1732
   * @private
1733
   */
1734
  updateDataType_() {
1735
    /** @type {!Object} */
1736
    this.dataType = {
1737
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
1738
      float: this.bitDepth == '32f' || this.bitDepth == '64',
1739
      signed: this.bitDepth != '8',
1740
      be: this.container == 'RIFX'
1741
    };
1742
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
1743
      this.dataType.bits = 8;
1744
      this.dataType.signed = false;
1745
    }
1746
  }
1747
1748
  /**
1749
   * Set the string code of the bit depth based on the 'fmt ' chunk.
1750
   * @private
1751
   */
1752
  bitDepthFromFmt_() {
1753
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
1754
      this.bitDepth = '32f';
1755
    } else if (this.fmt.audioFormat === 6) {
1756
      this.bitDepth = '8a';
1757
    } else if (this.fmt.audioFormat === 7) {
1758
      this.bitDepth = '8m';
1759
    } else {
1760
      this.bitDepth = this.fmt.bitsPerSample.toString();
1761
    }
1762
  }
1763
1764
  /**
1765
   * Validate the header of the file.
1766
   * @throws {Error} If any property of the object appears invalid.
1767
   * @private
1768
   */
1769
  validateHeader_() {
1770
    this.validateBitDepth_();
1771
    this.validateNumChannels_();
1772
    this.validateSampleRate_();
1773
  }
1774
1775
  /**
1776
   * Validate the bit depth.
1777
   * @return {boolean} True is the bit depth is valid.
1778
   * @throws {Error} If bit depth is invalid.
1779
   * @private
1780
   */
1781
  validateBitDepth_() {
1782
    if (!AUDIO_FORMATS[this.bitDepth]) {
1783
      if (parseInt(this.bitDepth, 10) > 8 &&
1784
          parseInt(this.bitDepth, 10) < 54) {
1785
        return true;
1786
      }
1787
      throw new Error('Invalid bit depth.');
1788
    }
1789
    return true;
1790
  }
1791
1792
  /**
1793
   * Validate the number of channels.
1794
   * @return {boolean} True is the number of channels is valid.
1795
   * @throws {Error} If the number of channels is invalid.
1796
   * @private
1797
   */
1798
  validateNumChannels_() {
1799
    /** @type {number} */
1800
    let blockAlign = this.fmt.numChannels * this.fmt.bitsPerSample / 8;
1801
    if (this.fmt.numChannels < 1 || blockAlign > 65535) {
1802
      throw new Error('Invalid number of channels.');
1803
    }
1804
    return true;
1805
  }
1806
1807
  /**
1808
   * Validate the sample rate value.
1809
   * @return {boolean} True is the sample rate is valid.
1810
   * @throws {Error} If the sample rate is invalid.
1811
   * @private
1812
   */
1813
  validateSampleRate_() {
1814
    /** @type {number} */
1815
    let byteRate = this.fmt.numChannels *
1816
      (this.fmt.bitsPerSample / 8) * this.fmt.sampleRate;
1817
    if (this.fmt.sampleRate < 1 || byteRate > 4294967295) {
1818
      throw new Error('Invalid sample rate.');
1819
    }
1820
    return true;
1821
  }
1822
1823
  /**
1824
   * Return the closest greater number of bits for a number of bits that
1825
   * do not fill a full sequence of bytes.
1826
   * @param {string} bitDepthCode The bit depth.
1827
   * @return {string}
1828
   * @private
1829
   */
1830
  realBitDepth_(bitDepthCode) {
1831
    if (bitDepthCode != '32f') {
1832
      bitDepthCode = (((parseInt(bitDepthCode, 10) - 1) | 7) + 1).toString();
1833
    }
1834
    return bitDepthCode;
1835
  }
1836
1837
  /**
1838
   * Return 'RIFF' if the container is 'RF64', the current container name
1839
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
1840
   * @return {string}
1841
   * @private
1842
   */
1843
  correctContainer_() {
1844
    return this.container == 'RF64' ? 'RIFF' : this.container;
1845
  }
1846
}
1847